package persistence.xstream;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import persistence.CorruptDataException;
import persistence.DatabaseConnectivityException;
import persistence.Persistent;
import persistence.SingleSessionPersistence;
import util.Log;
import util.NotFoundException;
import util.Subsystem;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

/**
 * persistence interface to xstream.

 * @author idan
 */
public class XStreamPersistenceController extends SingleSessionPersistence
{
	private Map<Long, Persistent> elements ;
	private String uri ;

	/**
	 * @param uri URI to load data from.
	 * @return object whose representation is in 'uri'.
	 */
	public static Object loadFromUri(final String uri)
	{
		try
		{
			Object objectFromXml = null ;

			String serializedObject = XStreamPersistenceController.getFileContents(uri) ;

			if (serializedObject.isEmpty())
			{
				return null ;
			}
			else
			{
				objectFromXml = XStreamPersistenceController.deserialize(serializedObject) ;
				return objectFromXml ;
			}
		}
		catch (final IOException e)
		{
			Log.getLogger(Subsystem.DATABASE_ACCESS).severe(e.getMessage()) ;
			throw new DatabaseConnectivityException(e) ;
		}
	}

	/**
	 * save an object to a URI.
	 * @param uri URI to save data to.
	 * @param data data to write to the file.
	 */
	public static void saveToUri(final String uri, final Object data)
	{
		try
		{
			PrintWriter outputStream ;
			outputStream = new PrintWriter(new FileWriter(uri)) ;
			final String serializedData = XStreamPersistenceController.serialize(data) ;
			outputStream.println(serializedData) ;
			outputStream.close() ;
		}
		catch (final IOException e)
		{
			Log.getLogger(Subsystem.DATABASE_ACCESS).severe(e.getMessage()) ;
			throw new DatabaseConnectivityException(e) ;
		}
	}

	/**
	 * Deserialize function is responsible for deserializing a string to an object.
	 *
	 * @param deserializeString the string we will deserialize.
	 * @return the object
	 */
	private static Object deserialize(final String deserializeString)
	{
		final XStream xStream = new XStream(new DomDriver()) ;
		return xStream.fromXML(deserializeString) ;
	}

	private static String getFileContents(final String uri) throws FileNotFoundException, IOException
	{
		final File dataFile = new File(uri) ;
		if (dataFile.getParentFile() != null)
		{
			dataFile.getParentFile().mkdirs() ;
		}
		if (!dataFile.exists())
		{
			dataFile.createNewFile() ;
		}
		BufferedReader inputStream ;
		inputStream = new BufferedReader(new FileReader(dataFile)) ;
		String temp = "" ;
		String serializedObject = "" ;
		while ((temp = inputStream.readLine()) != null)
		{
			serializedObject += temp ;
		}
		inputStream.close() ;
		return serializedObject ;
	}

	/**
	 * Serialize function is responsible for serializing an object to a string .
	 *
	 * @param serializedObject the serialized object
	 * @return serialized string
	 */
	private static String serialize(final Object serializedObject)
	{
		final XStream xStream = new XStream(new DomDriver()) ;
		return xStream.toXML(serializedObject) ;
	}

	/**
	 * @param uri URI of the persistent media.
	 * @param load whether to load the database from the URI or start a fresh one
	 * @throws CorruptDataException if tried to load from a file and the database did not
	 * contain the expected information.
	 * @pre there is not transaction taking place.
	 * @post
	 */
	public XStreamPersistenceController(final String uri, final boolean load) throws CorruptDataException
	{
		super() ;
		this.uri = uri ;
		if (load)
		{
			this.loadElementsFromFile() ;
		}
		else
		{
			this.deleteAll() ;
		}
	}

	/**
	 * @see persistence.PersistenceController#close()
	 */
	@Override
	public void close()
	{
		this.commitToDatabase() ;
		this.uri = null ;
	}

	/**
	 * @see persistence.PersistenceController#delete(long)
	 */
	@Override
	public void delete(final long id) throws NotFoundException
	{
		Persistent condemned = this.elements.remove(id) ;
		if (condemned == null)
		{
			throw new NotFoundException(id) ;
		}
	}

	/**
	 * @see persistence.PersistenceController#deleteAll()
	 */
	@Override
	public void deleteAll()
	{
		try
		{
			this.elements = new HashMap<Long, Persistent>() ;
			File physicalDatabase = new File(this.uri) ;
			physicalDatabase.delete() ;
			if (physicalDatabase.getParentFile() != null)
			{
				physicalDatabase.getParentFile().mkdirs() ;
			}
			if (!physicalDatabase.exists())
			{
				physicalDatabase.createNewFile() ;
			}
		}
		catch (IOException e)
		{
			throw new DatabaseConnectivityException(e) ;
		}
	}

	/**
	 * @see persistence.PersistenceController#get(long)
	 */
	@Override
	public Persistent get(final long id)
	{
		Persistent value = this.elements.get(id) ;
		return value ;
	}

	/**
	 * @see persistence.PersistenceController#getAll()
	 */
	@Override
	public Collection<Persistent> getAll()
	{
		return new HashSet<Persistent>(this.elements.values()) ;
	}

	/**
	 * @return URI with which this controller works.
	 */
	public String getUri()
	{
		return this.uri ;
	}

	/**
	 * @see persistence.PersistenceController#put(persistence.Persistent)
	 */
	@Override
	public void put(final Persistent obj)
	{
		long id = this.getLowestUnusedId() ;
		obj.setId(id) ;
		this.elements.put(obj.getId(), obj) ;
	}

	/**
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString()
	{
		return this.uri ;
	}

	/**
	 * @see persistence.SingleSessionPersistence#commitToDatabase()
	 */
	@Override
	protected void commitToDatabase()
	{
		this.saveElementsToFile() ;
	}

	private final long getLowestUnusedId()
	{
		long result = 1 ;
		while (this.elements.containsKey(result))
		{
			result++ ;
		}
		return result ;
	}

	/**
	 * the map is created empty if the file is empty.
	 * @throws CorruptDataException if the database does not contain the expected information.
	 */
	@SuppressWarnings("unchecked")
	private void loadElementsFromFile() throws CorruptDataException
	{
		Log.getLogger(Subsystem.DATABASE_ACCESS).finest("loading from database") ;
		this.elements = (Map<Long, Persistent>)XStreamPersistenceController.loadFromUri(this.uri) ;
		if (this.elements == null)
		{
			this.elements = new HashMap<Long, Persistent>() ;
		}
	}

	/**
	 * save the current state to a file.
	 */
	private void saveElementsToFile()
	{
		Log.getLogger(Subsystem.DATABASE_ACCESS).finest("saving to database") ;
		XStreamPersistenceController.saveToUri(this.uri, this.elements) ;
	}

}
